/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_kit/zhiqim_git.htm
 *
 * ZhiqimGit is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.git;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.zhiqim.git.objer.model.PersonIdent;
import org.zhiqim.git.objer.objectid.ObjectId;
import org.zhiqim.git.objer.objectid.RevObject;
import org.zhiqim.git.objer.objectid.RevTag;
import org.zhiqim.git.refer.Ref;
import org.zhiqim.git.refer.RefRenamer;
import org.zhiqim.git.refer.RefUpdater;
import org.zhiqim.git.refer.ref.ObjectIdRef;
import org.zhiqim.git.refer.ref.SymbolicRef;
import org.zhiqim.git.util.GitValidates;

import org.zhiqim.kernel.util.Bytes;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.filters.FileNameNotEndsWithFilter;

/**
 * 引用管理器，包括两种格式：松散型引用（Loose）、和打包型引用（Packed）两种，以及引用日志管理等。
 * 1、松散型引用存在${gitMetaDir}/refs目录下，引用名称和文件名相同，内容为引用的对象编码。
 * 2、打包型引用统一存储在${gitMetaDir}/packed-refs这个文件中，每个引用一行，相对于松散型来说减少文件个数。
 * 3、目前只实现松散式一种方式
 * 
 * 1        ${gitMetaDir}/refs
 * 1.1      ${gitMetaDir}/refs/heads
 * 1.2      ${gitMetaDir}/refs/tags
 * 2        ${gitMetaDir}/packed-refs
 * 3        ${gitMetaDir}/logs
 * 3.1      ${gitMetaDir}/logs/refs
 * 3.1.1    ${gitMetaDir}/logs/refs/heads
 *
 * @version v1.0.0 @author zhichenggang 2017-4-2 新建与整理
 */
public class GitRefer implements GitConstants
{
    /** 过滤文件名后缀为.lock的锁文件 */
    private static final FilenameFilter FILTER = new FileNameNotEndsWithFilter(_LOCK);
    
    private final Git git;
    private final File gitMetaDir;
    
    private final File refsDir;                 //松散型引用根目录${gitMetaDir}/refs
    private final File logsDir;                 //日志根目录
    private final File logsRefsDir;             //日志引用目录
    
    /******************************************************************************************/
    //构造函数&创建&关闭
    /******************************************************************************************/

    public GitRefer(Git git)
    {
        this.git = git;
        this.gitMetaDir = git.getMetaDir();
        this.refsDir = new File(gitMetaDir, P_REFS_);
        this.logsDir = new File(gitMetaDir, LOGS);
        this.logsRefsDir = new File(gitMetaDir, LOGS_REFS);
    }

    /**
     * 创建引用目录，包括/refs和/logs两大类
     * 
     * 1        ${gitMetaDir}/refs
     * 1.1      ${gitMetaDir}/refs/heads
     * 1.2      ${gitMetaDir}/refs/tags
     * 2        ${gitMetaDir}/logs
     * 2.1      ${gitMetaDir}/logs/refs
     * 2.1.1    ${gitMetaDir}/logs/refs/heads
     */
    public void create() throws IOException
    {
        //创建引用目录
        Files.mkDirectory(refsDir);
        Files.mkDirectory(new File(refsDir, HEADS));
        Files.mkDirectory(new File(refsDir, TAGS));
        
        //创建引用日志目录
        Files.mkDirectory(logsDir);
        Files.mkDirectory(logsRefsDir);
        Files.mkDirectory(new File(logsRefsDir, HEADS));
    }
    
    /** 关闭 */
    public void close()
    {
    }
    
    /******************************************************************************************/
    //构造函数&创建&关闭
    /******************************************************************************************/

    public Git git()
    {
        return git;
    }

    /**
     * 判断引用名称是否冲突，要求路径中目录和文件名都不相同，如refs/heads/b1和refs/heads/b1/abc是冲突的
     * 
     * @param name          引用名称
     * @return              =true表示冲突
     * @throws IOException  异常
     */
    public boolean isNameConflicting(String name) throws IOException
    {
        List<Ref> refList = getRefList();

        //1.先检查路径中前缀是否有相同的引用全称
        int lastSlash = name.lastIndexOf('/');
        while (lastSlash > 0)
        {
            String needle = name.substring(0, lastSlash);
            if (refList.contains(needle))
                return true;
            lastSlash = name.lastIndexOf('/', lastSlash - 1);
        }

        //2.再检查是否有以该名称为前缀的引用全称
        String prefix = name + '/';
        if (GitValidates.isPrefix(refList, prefix))
            return true;
        
        return false;
    }

    /**
     * 读出一个引用，支持引用仅提供短名称，从标准引用前缀中查找
     * 
     * @param name          引用全称或短称
     * @return              引用对象
     * @throws IOException  异常
     */
    public Ref findRef(final String name) throws IOException
    {
        for (String prefix : P_REFS_PATH)
        {
            Ref ref = scanRef(prefix + name);
            if (ref != null)
                return ref;
        }
        
        return null;
    }
    
    /**
     * 通过引用全称或短称通出引用对象，如果存在返回对应的对象编号，不存在返回null
     * 
     * @param refName       引用全称或短称
     * @return              对象编号
     * @throws IOException  异常
     */
    public ObjectId getObjectId(String refName) throws IOException
    {
        Ref ref = findRef(refName);
        return ref == null?null:ref.getObjectId();
    }
    
    /**
     * 获取所有引用列表，不包含HEAD
     * 
     * @return              引用列表
     * @throws IOException  异常
     */
    public List<ObjectId> getRefObjectIdList() throws IOException
    {
        ArrayList<Ref> list = new ArrayList<>();
        scanDirectroy(list, P_REFS_, refsDir);
        
        ArrayList<ObjectId> oidList = new ArrayList<>();
        for (Ref ref : list)
        {
            oidList.add(ref.getObjectId());
        }
        oidList.trimToSize();
        
        return oidList;
    }
    
    /**
     * 获取所有引用列表
     * 
     * @return              引用列表
     * @throws IOException  异常
     */
    public List<Ref> getRefList() throws IOException
    {
        ArrayList<Ref> list = new ArrayList<>();
        Ref head = scanRef(HEAD);
        if (head != null)
            list.add(head);
        
        scanDirectroy(list, P_REFS_, refsDir);
        list.trimToSize();
        
        return list;
    }
    
    /**
     * 获取前缀对应的引用列表
     * 
     * @param prefix        引用
     * @return              引用列表
     * @throws IOException  异常
     */
    public List<Ref> getRefList(String prefix) throws IOException
    {
        ArrayList<Ref> list = new ArrayList<>();
        File dir = new File(refsDir, prefix.substring(P_REFS_.length()));
        scanDirectroy(list, prefix, dir);
        list.trimToSize();
        
        return list;
    }

    /**
     * 通过前缀读取对应的引用表
     * 
     * @return              引用表
     * @throws IOException  异常
     */
    public Map<String, Ref> getRefMap() throws IOException
    {
        List<Ref> list = getRefList();
        
        HashMap<String, Ref> map = new HashMap<>();
        for (Ref ref : list)
        {
            map.put(ref.getName(), ref);
        }
        
        return map;
    }
    
    /**
     * 通过前缀读取对应的引用表
     * 
     * @param prefix        引用
     * @return              引用表
     * @throws IOException  异常
     */
    public Map<String, Ref> getRefMap(String prefix) throws IOException
    {
        List<Ref> list = getRefList(prefix);
        
        HashMap<String, Ref> map = new HashMap<>();
        for (Ref ref : list)
        {
            map.put(ref.getName(), ref);
        }
        
        return map;
    }
    
    /**
     * 判断引用是否存在
     * 
     * @param fullName      引用全称
     * @return              =true表示存在
     * @throws IOException  异常
     */
    public boolean hasRef(String fullName) throws IOException
    {
        return fileFor(fullName).exists();
    }

    /**
     * 通过名称查找对应的引用，引用快捷方式或对象引用
     * 
     * @param fullName      引用名称
     * @return              引用对象
     * @throws IOException  异常
     */
    public Ref scanRef(String fullName) throws IOException
    {
        File path = fileFor(fullName);
        if (!Files.exists(path))
            return null;
        
        String content = Strings.trim(Files.readUTF8(path));
        if (GitValidates.isSymRef(content))
        {//快捷方式
            String target = content.substring(5);
            
            SymbolicRef ref= new SymbolicRef(fullName, new ObjectIdRef(target));
            return resolve(ref, 0);
        }
        else
        {//对象引用
            if (content.length() != LEN_OBJECT_ID_STRING)
                return null;
        
            ObjectId id = ObjectId.parse(content);
            return new ObjectIdRef(fullName, id);
        }
    }
    
    /** 查找目录 */
    private void scanDirectroy(List<Ref> refList, String prefix, File dir) throws IOException
    {
        final String[] entries = dir.list(FILTER);
        if (entries != null && 0 < entries.length)
        {
            Arrays.sort(entries);
            for (String name : entries)
            {
                File e = new File(dir, name);
                if (e.isDirectory())
                    scanDirectroy(refList, prefix + name + '/', e);
                else
                {
                    Ref ref = scanRef(prefix + name);
                    if (ref != null)
                        refList.add(ref);
                }
            }
        }
    }

    /**
     * 通过名称找到对应的文件
     * 
     * @param name      文件相对路径
     * @return          文件对象
     */
    public File fileFor(String name)
    {
        if (name.startsWith(P_REFS_))
        {
            name = name.substring(P_REFS_.length());
            return new File(refsDir, name);
        }
        
        return new File(gitMetaDir, name);
    }

    /**
     * 通过引用名称找到日志文件
     * 
     * @param name      引用名称
     * @return          日志文件对象
     */
    public File logFor(String name)
    {
        if (name.startsWith(P_REFS_))
        {
            name = name.substring(P_REFS_.length());
            return new File(logsRefsDir, name);
        }
        
        return new File(logsDir, name);
    }
    
    /**
     * 剥除附注标签的引用，当引用为标签引用时有效
     * 
     * @param ref           标签引用
     * @return              剥除后的引用
     * @throws IOException  异常
     */
    public Ref peel(Ref ref) throws IOException
    {
        ObjectIdRef target = ref.getUltimateTarget();
        if (target.isPeeled() || target.getObjectId() == null)
        {//已剥除对象的引用或未加载对象的引用
            return ref;
        }
        
        //创建一个新的目标对象引用，并设置状态为已剥除
        ObjectIdRef newTarget = new ObjectIdRef(target);
        newTarget.setPeeled();
        
        //加载对象，查看是否标签
        GitWalker walker = new GitWalker(git());
        RevObject obj = walker.parse(target.getObjectId());
        if (obj instanceof RevTag)
        {//如果是标签，剥除找到最终对象
            newTarget.setPeeledObjectId(walker.peel(obj));
        }

        //重建引用
        return recreate(ref, newTarget);
    }
    
    /** 递归剥除多级快捷映射，得到对象引用或一级快捷方式引用 */
    private Ref recreate(Ref old, ObjectIdRef target)
    {
        if (old.isSymbolic())
        {
            Ref ref = recreate(old.getTarget(), target);
            return new SymbolicRef(old.getName(), ref);
        }
        return target;
    }
    
    /** 找到最近的一个快捷方式引用 */
    private Ref resolve(Ref ref, int depth) throws IOException
    {
        if (!ref.isSymbolic())
            return ref;
        
        Ref dst = ref.getTarget();

        if (MAX_SYMBOLIC_REF_DEPTH <= depth)
        {//超过最大快捷方式次数的表示不存在
            return null;
        }
        
        dst = scanRef(dst.getName());
        if (dst == null)
            return ref;
        
        dst = resolve(dst, depth+1);
        if (dst == null)
            return null;
        
        return new SymbolicRef(ref.getName(), dst);
    }
    
    
    /******************************************************************************************/
    //操作
    /******************************************************************************************/

    /**
     * 创建一个更新命令，用于写入到临时引用中
     * 
     * @return              临时引用的更新命令
     * @throws IOException  访问文件异常
     */
    public RefUpdater newTemporaryUpdater() throws IOException
    {
        File tmpFile = File.createTempFile("renamed_", "_ref", refsDir);
        String name = P_REFS_ + tmpFile.getName();
        Ref ref = new ObjectIdRef(name);
        return new RefUpdater(this, ref);
    }
    
    /**
     * 创建一个更新命令，用于（创建、更新和删除）操作
     * 
     * @param name          引用名称
     * @param detach        是否对快捷方式引用作处理
     * @return              引用更新器
     * @throws IOException  访问引用异常
     */
    public RefUpdater newUpdater(String name, boolean detach) throws IOException
    {
        Ref ref = scanRef(name);
        
        if (ref == null){
            ref = new ObjectIdRef(name);
        }else if (detach && ref.isSymbolic()){
            ref = new ObjectIdRef(name, ref.getObjectId());
        }
        
        return new RefUpdater(this, ref);
    }

    /**
     * 创建一个重命名命令，用于重命名引用操作
     * 
     * @param fromName      引用原名称
     * @param toName        引用新名称
     * @return              引用重命名命令
     * @throws IOException  访问引用异常
     */
    public RefRenamer newRenamer(String fromName, String toName) throws IOException
    {
        RefUpdater from = newUpdater(fromName, false);
        RefUpdater to = newUpdater(toName, false);
        return new RefRenamer(from, to);
    }
    
    /**
     * 删除操作，指定一个更新器
     * 
     * @param update        更新器
     * @throws IOException  文件访问异常
     */
    public void delete(RefUpdater update) throws IOException
    {
        Ref ref = update.getRef().getUltimateTarget();
        String name = ref.getName();

        int depth = Strings.getTimes(name, '/') - 2;
        Files.deleteFile(logFor(name), depth);
        if (ref.isStored())
        {
            update.unlock();
            Files.deleteFile(fileFor(name), depth);
        }
    }

    /**
     * 写入操作日志
     * 
     * @param update        更新器
     * @param msg           消息
     * @param deref         是否对快捷方式引用作处理
     * @throws IOException  文件IO异常
     */
    public void log(RefUpdater update, String msg, boolean deref) throws IOException
    {
        final ObjectId oldId = update.getOldObjectId();
        final ObjectId newId = update.getNewObjectId();
        final Ref ref = update.getRef();

        PersonIdent ident = update.getRefLogIdent();
        if (ident == null)
            ident = new PersonIdent(git);
        else
            ident = new PersonIdent(ident);

        StringBuilder strb = new StringBuilder();
        strb.append(ObjectId.toString(oldId));
        strb.append(_SPACE_);
        strb.append(ObjectId.toString(newId));
        strb.append(_SPACE_);
        strb.append(ident.toExternalString());
        strb.append(_HT_);
        strb.append(msg);
        strb.append(_LF_);
        
        byte[] content = Bytes.BU.toBytes(strb.toString());
        
        logToFile(ref.getName(), content);
        if (deref && ref.isSymbolic())
        {//快捷方式引用插入两条
            logToFile(ref.getUltimateTarget().getName(), content);
        }
    }

    /** 写入内容到文件 */
    private void logToFile(final String refName, final byte[] rec) throws IOException
    {
        File file = logFor(refName);
        boolean write;
        if (git.getConfig().isLogAllRefUpdates() && GitValidates.shouldAutoCreateLog(refName))
            write = true;
        else if (file.isFile())
            write = true;
        else
            write = false;

        if (write)
        {//增量写入
            Files.write(file, rec, true);
        }
    }
}
